package coap.messages;

import java.nio.ByteBuffer;
import java.util.Arrays;


public class Option implements Comparable {

    int optionNumber;
    byte[] optionValue;

    public Option(int previousOptionNumber, ByteBuffer buffer) {
        this.deserialize(previousOptionNumber, buffer);
    }

    public Option(int optionNumber, byte[] optionValue) {
        switch (optionNumber) {
            case OptionNumber.Etag:
            case OptionNumber.Token:
                if (optionValue.length < 1 || optionValue.length > 8)
                    throw new RuntimeException("size must be between 1 and 8");
                break;
            case OptionNumber.IfMatch:
                if (optionValue.length > 8)
                    throw new RuntimeException("size must be <= 8");
                break;
            case OptionNumber.IfNoneMatch:
                if (optionValue.length > 0)
                    throw new RuntimeException("size must be == 0");
                break;
            default:
                throw new RuntimeException("Option " + OptionNumber.getString(optionNumber) + " should not have a byte[] value");
        }
        if (optionValue.length > 270)
            throw new RuntimeException("optionValue too long " + optionValue.length);
        this.optionNumber = optionNumber;
        this.optionValue = optionValue;
    }

    public Option(int optionNumber, int value) {
        if (value < 0)
            throw new RuntimeException("value must > 0");
        switch (optionNumber) {
            case OptionNumber.ContentType:
            case OptionNumber.UriPort:
            case OptionNumber.Accept:
                if (value >= 65536)
                    throw new RuntimeException("value too large");
                break;
            case OptionNumber.MaxAge:
                break;
            default:
                throw new RuntimeException("Option " + OptionNumber.getString(optionNumber) + " should not have a int value");
        }
        this.optionNumber = optionNumber;
        if (value == 0) {
            this.optionValue = new byte[0];
        } else if (value < 256) {
            this.optionValue = new byte[1];
            ByteBuffer b = ByteBuffer.wrap(this.optionValue);
            b.put((byte) value);
        } else if (value < 65536) {
            this.optionValue = new byte[2];
            ByteBuffer b = ByteBuffer.wrap(this.optionValue);
            b.putShort((short) value);
        } else {
            this.optionValue = new byte[4];
            ByteBuffer b = ByteBuffer.wrap(this.optionValue);
            b.putInt(value);
        }
    }

    public Option(int optionNumber, String value) {
        switch (optionNumber) {
            case OptionNumber.ProxyUri:
            case OptionNumber.UriHost:
            case OptionNumber.LocationPath:
            case OptionNumber.LocationQuery:
            case OptionNumber.UriPath:
            case OptionNumber.UriQuery:
                break;
            default:
                throw new RuntimeException("Option " + OptionNumber.getString(optionNumber) + " should not have a string value");
        }
        this.optionNumber = optionNumber;
        byte[] tmp = value.getBytes();
        if (tmp.length == 0 || tmp.length > 270)
            throw new RuntimeException("String too short or long");
        this.optionValue = tmp;
    }

    public boolean isCritical() {
        switch (this.optionNumber) {
            case OptionNumber.ContentType:
            case OptionNumber.ProxyUri:
            case OptionNumber.UriHost:
            case OptionNumber.UriPort:
            case OptionNumber.UriPath:
            case OptionNumber.Token:
            case OptionNumber.IfMatch:
            case OptionNumber.UriQuery:
            case OptionNumber.IfNoneMatch:
                return true;
        }
        return false;
    }

    public int getOptionNumber() {
        return this.optionNumber;
    }

    public byte[] getOptionValue() {
        switch (optionNumber) {
            case OptionNumber.Etag:
            case OptionNumber.Token:
            case OptionNumber.IfMatch:
            case OptionNumber.IfNoneMatch:
                break;
            default:
                throw new RuntimeException("Option " + OptionNumber.getString(optionNumber) + " should not have a byte[] value");
        }
        return this.optionValue;
    }

    public int getIntValue() {
        switch (optionNumber) {
            case OptionNumber.ContentType:
            case OptionNumber.UriPort:
            case OptionNumber.Accept:
            case OptionNumber.MaxAge:
                break;
            default:
                throw new RuntimeException("Option " + OptionNumber.getString(optionNumber) + " should not have a int value");
        }
        ByteBuffer b = ByteBuffer.wrap(this.optionValue);
        int ret;
        if (this.optionValue.length == 0)
            return 0;
        else if (this.optionValue.length == 1) {
            ret = b.get();
            if (ret < 0)
                ret += 256;
            return ret;
        } else if (this.optionValue.length == 2) {
            ret = b.getShort();
            if (ret < 0)
                ret += 65536;
            return ret;
        } else if (this.optionValue.length == 3) {
            throw new RuntimeException("weird");
        } else if (this.optionValue.length == 4) {
            ret = b.getInt();
            return ret;
        } else
            throw new RuntimeException("weird");
    }

    public String getStringValue() {
        switch (optionNumber) {
            case OptionNumber.ProxyUri:
            case OptionNumber.UriHost:
            case OptionNumber.LocationPath:
            case OptionNumber.LocationQuery:
            case OptionNumber.UriPath:
            case OptionNumber.UriQuery:
                break;
            default:
                throw new RuntimeException("Option " + OptionNumber.getString(optionNumber) + " should not have a string value");
        }
        return new String(this.optionValue);
    }

    public void serialize(int previousOptionNumber, ByteBuffer buffer) {
        byte firstByte = 0;
        int optionDelta = optionNumber - previousOptionNumber;
        if (optionDelta > OptionNumber.Fenceposting) {
            throw new RuntimeException("werid thing happening, optionNumber=" + optionNumber + " previous=" + previousOptionNumber);
        }
        firstByte |= (byte) (optionDelta << 4);
        int length = optionValue.length;
        if (length <= 14) {
            firstByte |= (byte) (length);
            buffer.put(firstByte);
        } else {
            firstByte |= (byte) 15;
            buffer.put(firstByte);
            byte secondByte = (byte) (length - 15);
            buffer.put(secondByte);
        }
        buffer.put(optionValue);
    }

    private void deserialize(int previousOptionNumber, ByteBuffer buffer) {
        byte firstByte = buffer.get();
        int optionDelta = (int) ((firstByte >> 4) & 15);
        this.optionNumber = previousOptionNumber + optionDelta;
        int length = 0;
        int shortLength = firstByte & 15;
        if (shortLength < 15) {
            length = shortLength;
        } else {
            byte tmp = buffer.get();
            if (tmp >= 0)
                length = tmp + 15;
            else
                length = tmp + 15 + 256;
        }

        optionValue = new byte[length];
        buffer.get(optionValue);
    }

    public String toString() {
        return OptionNumber.getString(optionNumber) + " length=" + optionValue.length;
    }

    public static void main(String[] args) {
        byte[] b = new byte[4096];
        ByteBuffer buffer = ByteBuffer.wrap(b);
        Option o1 = new Option(OptionNumber.ContentType, new byte[0]);
        Option fence = new Option(OptionNumber.Fenceposting, new byte[0]);
        Option o2 = new Option(OptionNumber.IfNoneMatch, new byte[15]);
        o1.serialize(0, buffer);
        fence.serialize(o1.getOptionNumber(), buffer);
        o2.serialize(fence.getOptionNumber(), buffer);
        buffer.flip();
        Option oo1 = new Option(0, buffer);
        Option oo2 = new Option(oo1.getOptionNumber(), buffer);
        Option oo3 = new Option(oo2.getOptionNumber(), buffer);
        System.out.println(oo1);
        System.out.println(oo2);
        System.out.println(oo3);
    }

    public int compareTo(Object arg0) {
        Option o = (Option) arg0;
        if (this.optionNumber < o.optionNumber)
            return -1;
        else if (this.optionNumber > o.optionNumber)
            return 1;
        else
            return 0;
    }

    @Override
    public boolean equals(Object o) {
        Option t = (Option) o;
        if (t.optionNumber != this.optionNumber)
            return false;
        return Arrays.equals(t.optionValue, this.optionValue);
    }

}
